package contegor.scire.structure;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.Map.Entry;

import contegor.scire.core.Action;
import contegor.scire.core.Concept;
import contegor.scire.core.Connectable;
import contegor.scire.core.ConnectableImpl;
import contegor.scire.core.Id;
import contegor.scire.core.Identifiable;
import contegor.scire.core.Link;
import contegor.scire.core.Primitive;
import contegor.scire.core.ConnectableImpl.Status;
import contegor.scire.core.modifications.BaseCreateConcept;
import contegor.scire.core.modifications.CreateAction;
import contegor.scire.core.modifications.CreateConcept;
import contegor.scire.core.modifications.CreateLink;
import contegor.scire.core.modifications.CreatePrimitive;
import contegor.scire.core.modifications.DeleteConnectable;
import contegor.scire.core.modifications.Modification;
import contegor.scire.core.modifications.SetPrimitiveValue;

class ContextChangesListener implements ContextListener {

  private static class Change {
    ConnectableImpl.Status status;
    long timestamp;

    public Change(Status status, long timestamp) {
      super();
      this.status = status;
      this.timestamp = timestamp;
    }
  }

  private static class InferredTask implements Comparable<InferredTask>{
    Modification<? extends Connectable> task;
    ConnectableImpl.Status status;
    long timestamp;

    public InferredTask(Modification<? extends Connectable> task, ConnectableImpl.Status status, long timestamp) {
      super();
      this.task = task;
      this.status = status;
      this.timestamp = timestamp;
    }

    @Override
    public int compareTo(InferredTask rhs) {
      if (this.timestamp < rhs.timestamp)
        return -1;
      if (this.timestamp > rhs.timestamp)
        return 1;
      if (this.status != rhs.status) { // Erased will be the last to be processed.
        if (this.status == Status.ERASED)
          return 1; 
        if (rhs.status == Status.ERASED)
          return -1;
      }
      if (this.status == rhs.status) {
        if (this.status == Status.ERASED)
          return rhs.task.getDepth() - this.task.getDepth();
      }

      if (this.task instanceof BaseCreateConcept<?>)
        return -1;
      if (rhs.task instanceof BaseCreateConcept<?>)
        return 1;
      return this.task.getDepth() - rhs.task.getDepth();
    }

    @Override
    public String toString() {
      return "InferredTask [timestamp=" + timestamp + ", status=" + status + ", task=" + task + "]";
    }
  }

  private Map<Connectable, List<Change>> changes;

  public ContextChangesListener() {
    this.changes = Collections.synchronizedMap(new HashMap<Connectable, List<Change>>());
  }

  private void newChange(Connectable element, ConnectableImpl.Status status) {
    if ((element instanceof ConnectableImpl) && ((ConnectableImpl)element).getStatus() == Status.UNTOUCHED)
      return;
    List<Change> changesList = this.changes.get(element);
    if (changesList == null) {
      synchronized(this.changes) {
        if (!this.changes.containsKey(element))
          this.changes.put(element, changesList = new Vector<Change>());
      }
    }
    changesList.add(new Change(status, System.currentTimeMillis()));
  }

  @Override
  public void added(Connectable element) {
    newChange(element, Status.CREATED);
  }

  @Override
  public void deleted(Connectable element) {
    newChange(element, Status.ERASED);
  }

  @Override
  public void updated(Connectable element) {
    newChange(element, Status.MODIFIED);
  }

  // Inferred 
  private InferredTask inferTask(Connectable connectable, List<Change> changes) {
    InferredTask itask = null;
    // Changes in 'List<Change> changes' came chronologically ordered.
    for (Change change: changes) {
      switch (change.status) {
        case ERASED:
          if ((itask == null) || (itask.task instanceof SetPrimitiveValue)) {
            List<Id> nextPoints = new LinkedList<Id>();
            while (connectable instanceof Link) {
              Link aux = ((Link)connectable);
              nextPoints.add(0, aux.getTarget());
              connectable = aux.getSource();
            }
            Id conceptId = ((Identifiable)connectable).getId();
            itask = new InferredTask(new DeleteConnectable(conceptId.getParentId(), conceptId.getLastIdFragment(), nextPoints.toArray(new Id[nextPoints.size()])), change.status, change.timestamp); 
          }
          else if ((itask.task instanceof CreateLink) || (itask.task instanceof BaseCreateConcept<?>)) {
            itask = null; // Create + Delete = null.
          }
        break;
        case CREATED:
          if (itask == null) {
            if (connectable instanceof Primitive) {
              Primitive primitive = (Primitive)connectable;
              itask = new InferredTask(new CreatePrimitive(primitive.getId().getParentId(), primitive.getId().getLastIdFragment(), primitive.getValue()), change.status, change.timestamp);
            }
            else if (connectable instanceof Action) {
              Action action = (Action)connectable;
              itask = new InferredTask(new CreateAction(action.getId().getParentId(), action.getId().getLastIdFragment(), action.getPlugIn(), action.getImplementation()), change.status, change.timestamp);
            }
            else if (connectable instanceof Concept) {
              Id id = ((Concept)connectable).getId();
              itask = new InferredTask(new CreateConcept(id.getParentId(), id.getLastIdFragment()), change.status, change.timestamp);
            }
            else if (connectable instanceof Link) {
              Link link = (Link)connectable;
              List<Id> nextPoints = new LinkedList<Id>();
              connectable = link.getSource();
              while (connectable instanceof Link) {
                Link aux = ((Link)connectable);
                nextPoints.add(0, aux.getTarget());
                connectable = aux.getSource();
              }
              Id conceptId = ((Identifiable)connectable).getId();
              itask = new InferredTask(new CreateLink(link.getFlow(), conceptId.getParentId(), link.getTarget(), conceptId.getLastIdFragment(), nextPoints.toArray(new Id[nextPoints.size()])), change.status, change.timestamp);
            }            
          }
          else if (itask.task instanceof DeleteConnectable) {
            if (connectable instanceof Primitive) {
              // Delete Primitive + Create Primitive = Modify Primitive.
              Primitive primitive = (Primitive)connectable;
              itask = new InferredTask(new SetPrimitiveValue(primitive.getId(), primitive.getValue()), change.status, change.timestamp);
            }
            else {
              // Delete + Create = null.
              itask = null;
            }
          }
          else {
            throw new IllegalArgumentException("Task different from Delete before a Create for: " + connectable);            
          }
        break;
        case MODIFIED:
          if ((itask == null) || !(itask.task instanceof DeleteConnectable)) {
            if (connectable instanceof Primitive) {
              Primitive primitive = (Primitive)connectable;
              itask = new InferredTask(new SetPrimitiveValue(primitive.getId(), primitive.getValue()), change.status, change.timestamp);
            }
          }
        break;
      }
    }
    return itask;
  }

  public List<Modification<? extends Connectable>> extractOrderedChanges() {
    List<InferredTask> itasks = new ArrayList<InferredTask>(this.changes.size());
    synchronized (this.changes) {
      for (Entry<Connectable, List<Change>> entry: changes.entrySet()) {
        InferredTask itask = inferTask(entry.getKey(), entry.getValue());
        if (itask != null)
          itasks.add(itask);
      }
      this.changes.clear();
    }
    Collections.sort(itasks);
    List<Modification<? extends Connectable>> tasks = new ArrayList<Modification<? extends Connectable>>(itasks.size());
    for (InferredTask itask: itasks)
      tasks.add(itask.task);
    return tasks;
  }

  public void clear() {
    this.changes.clear();
  }
}
